package edu.thu.thss.yxy.sip;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;

import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;

import edu.thu.thss.yxy.sip.event.CallEvent;
import edu.thu.thss.yxy.sip.sdp.SdpUtils;
import edu.thu.thss.yxy.util.Logger;

/**
 * Implements all call management logic
 * 
 * @author Andy Gao
 * 
 */

@SuppressWarnings("unused")
public class SipTelephonyProcessor {
	/**
	 * Our class logger.
	 */
	private static final Logger logger = Logger
			.getLogger(SipTelephonyProcessor.class);

	/**
	 * A reference to the <tt>SipManager</tt> instance that created us.
	 */
	private final SipManager sipMgrCallback;

	/**
	 * Contains references for all currently active (non ended) calls.
	 */
	private final ActiveCallsRepository activeCallsRepository = new ActiveCallsRepository();

	/**
	 * The SIP transaction that established this call. This was previously kept
	 * in the jain-sip dialog but got deprecated there so we're now keeping it
	 * here.
	 */
	private Transaction latestInviteTransaction = null;

	/**
	 * Creates a new instance and adds itself as an <tt>INVITE</tt> method
	 * handler in the creating sipManager.
	 * 
	 * @param sipMrgCallback
	 *            a reference to the <tt>SipManager</tt> instance that
	 *            createdus.
	 */
	public SipTelephonyProcessor(SipManager sipMrgCallback) {
		this.sipMgrCallback = sipMrgCallback;
	}

	/**
	 * Returns an iterator over all currently active calls.
	 * 
	 * @return an iterator over all currently active calls.
	 */
	public Iterator<Call> getActiveCalls() {
		return activeCallsRepository.getActiveCalls();
	}

	/**
	 * Returns a reference to the {@link ActiveCallsRepository} that we are
	 * currently using.
	 * 
	 * @return a reference to the {@link ActiveCallsRepository} that we are
	 *         currently using.
	 */
	protected ActiveCallsRepository getActiveCallsRepository() {
		return activeCallsRepository;
	}

	/**
	 * Sets the transaction instance that contains the INVITE which started this
	 * call.
	 * 
	 * @param transaction
	 *            the Transaction that initiated this call.
	 */
	public void setLatestInviteTransaction(Transaction transaction) {
		this.latestInviteTransaction = transaction;
	}

	/**
	 * Returns the transaction instance that contains the INVITE which started
	 * this call.
	 * 
	 * @return the Transaction that initiated this call.
	 */
	public Transaction getLatestInviteTransaction() {
		return latestInviteTransaction;
	}

	/**
	 * Processes a Request received on a SipProvider upon which this SipListener
	 * is registered.
	 * <p>
	 * 
	 * @param requestEvent
	 *            requestEvent fired from the SipProvider to the
	 *            <tt>SipListener</tt> representing a Request received from the
	 *            network.
	 * 
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processRequest(RequestEvent requestEvent) {
		ServerTransaction serverTransaction = requestEvent
				.getServerTransaction();
		SipProvider jainSipProvider = (SipProvider) requestEvent.getSource();
		Request request = requestEvent.getRequest();
		String requestMethod = request.getMethod();

		if (serverTransaction == null) {

			try {
				serverTransaction = jainSipProvider.getNewServerTransaction(request);
			} catch (TransactionAlreadyExistsException ex) {
				// let's not scare the user and only log a message
				logger.error("Failed to create a new server"
						+ "transaction for an incoming request\n"
						+ "(Next message contains the request)", ex);
				return false;
			} catch (TransactionUnavailableException ex) {
				// let's not scare the user and only log a message
				logger.error("Failed to create a new server"
						+ "transaction for an incoming request\n"
						+ "(Next message contains the request)", ex);
				return false;
			}
		}

		boolean processed = false;

		// INVITE
		if (requestMethod.equals(Request.INVITE)) {
			logger.debug("received INVITE");
			DialogState dialogState = serverTransaction.getDialog().getState();
			if ((dialogState == null)
					|| dialogState.equals(DialogState.CONFIRMED)) {
				if (logger.isDebugEnabled())
					logger.debug("request is an INVITE. Dialog state="
							+ dialogState);
				processInvite(jainSipProvider, serverTransaction);
				processed = true;
			} else {
				logger.error("reINVITEs while the dialog is not "
						+ "confirmed are not currently supported.");
			}
		}
		// ACK
		else if (requestMethod.equals(Request.ACK)) {
			processAck(serverTransaction, request);
			processed = true;
		}
		// BYE
		else if (requestMethod.equals(Request.BYE)) {
			processBye(serverTransaction, request);
			processed = true;
		}
		// CANCEL
		else if (requestMethod.equals(Request.CANCEL)) {
			processCancel(serverTransaction, request);
			processed = true;
		}

		return processed;
	}

	/**
	 * Analyzes the incoming <tt>responseEvent</tt> and then forwards it to the
	 * proper event handler.
	 * 
	 * @param responseEvent
	 *            the responseEvent that we received ProtocolProviderService.
	 * 
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processResponse(ResponseEvent responseEvent) {
		ClientTransaction clientTransaction = responseEvent.getClientTransaction();

		Response response = responseEvent.getResponse();

		CSeqHeader cseq = ((CSeqHeader) response.getHeader(CSeqHeader.NAME));

		if (cseq == null) {
			logger.error("An incoming response did not contain a CSeq header");
		}

		String method = cseq.getMethod();

		SipProvider sourceProvider = (SipProvider) responseEvent.getSource();

		int responseStatusCode = response.getStatusCode();
		boolean processed = false;
		switch (responseStatusCode) {

		// OK
		case Response.OK:
			if (method.equals(Request.INVITE)) {
				processInviteOK(clientTransaction, response);
				processed = true;
			}
			// Ignore the case of method.equals(Request.BYE)
			break;

		// Ringing
		case Response.RINGING:
			processRinging(clientTransaction, response);
			processed = true;
			break;

		// Trying
		case Response.TRYING:
			processTrying(clientTransaction, response);
			processed = true;
			break;

		// Busy
		case Response.BUSY_HERE:
		case Response.BUSY_EVERYWHERE:
		case Response.DECLINE:
			processBusyHere(clientTransaction, response);
			processed = true;
			break;

		// 401 UNAUTHORIZED
		case Response.UNAUTHORIZED:
		case Response.PROXY_AUTHENTICATION_REQUIRED:
			processAuthenticationChallenge(clientTransaction, response,
					sourceProvider);
			processed = true;
			break;
		case Response.REQUEST_TERMINATED: {
			Call call = activeCallsRepository.findCall(clientTransaction
					.getDialog());
			if (call != null) {
				call.setCallState(CallState.FAILED);
			}
			processed = true;
		}
			break;
		// errors
		default:
			int responseStatusCodeRange = responseStatusCode / 100;

			if ((responseStatusCodeRange == 4)
					|| (responseStatusCodeRange == 5)
					|| (responseStatusCodeRange == 6)) {
				Call call = activeCallsRepository.findCall(clientTransaction
						.getDialog());

				logger.error("Received error: " + response.getStatusCode()
						+ " " + response.getReasonPhrase());

				if (call != null)
					call.setCallState(CallState.FAILED);

				processed = true;
			}
			// ignore everything else.
			break;
		}
		return processed;
	}

	/**
	 * Updates the call state of the corresponding call peer.
	 * 
	 * @param clientTransaction
	 *            the transaction in which the response was received.
	 * @param response
	 *            the trying response.
	 */
	private void processTrying(ClientTransaction clientTransaction,
			Response response) {
		Dialog dialog = clientTransaction.getDialog();

		// find the call
		Call call = activeCallsRepository.findCall(dialog);

		if (call == null) {
			logger.debug("Received a stray trying response.");
			return;
		}

		// change status
		call.setCallState(CallState.DIALING);
	}

	/**
	 * Updates the call state of the corresponding call peer. We'll also try to
	 * extract any details here that might be of use for call peer presentation
	 * and that we didn't have when establishing the call.
	 * 
	 * @param clientTransaction
	 *            the transaction in which the response was received.
	 * @param response
	 *            the Trying response.
	 */
	private void processRinging(ClientTransaction clientTransaction,
			Response response) {
		Dialog dialog = clientTransaction.getDialog();
		// find the call
		Call call = activeCallsRepository.findCall(dialog);

		if (call == null) {
			logger.debug("Received a stray trying response.");
			return;
		}
		// change status.
		call.setCallState(CallState.RINGING);
	}

	/**
	 * Sets to CONNECTED that state of the corresponding call peer and sends an
	 * ACK.
	 * 
	 * @param clientTransaction
	 *            the <tt>ClientTransaction</tt> that the response arrived in.
	 * @param ok
	 *            the OK <tt>Response</tt> to process
	 */
	private void processInviteOK(ClientTransaction clientTransaction,
			Response ok) {
		Dialog dialog = clientTransaction.getDialog();
		// find the call
		Call call = activeCallsRepository.findCall(dialog);

		if (call == null) {
			// there's definitely no dialog.
			logger.debug("Received a stray ok response.");
			return;
		}

		// send ACK
		try {
			CSeqHeader cseq = ((CSeqHeader) clientTransaction.getRequest()
					.getHeader(CSeqHeader.NAME));

			Request ack = clientTransaction.getDialog().createAck(
					cseq.getSeqNumber());
			clientTransaction.getDialog().sendAck(ack);

		} catch (InvalidArgumentException ex) {
			logger.error("Error creating an ACK (CSeq?)", ex);
			call.setCallState(CallState.DISCONNECTED);
			return;
		} catch (SipException ex) {
			logger.error("Failed to create ACK request!", ex);
			call.setCallState(CallState.DISCONNECTED);
			return;
		}

		call.setRemoteSdpDescription(new String(ok.getRawContent()));
		// change status
		if (!call.getCallState().equals(CallState.CONNECTED)) {
			call.setCallState(CallState.CONNECTED);
		}
	}

	/**
	 * Sets corresponding state to the call peer associated with this
	 * transaction.
	 * 
	 * @param clientTransaction
	 *            the transaction in which
	 * @param busyHere
	 *            the busy here Response
	 */
	private void processBusyHere(ClientTransaction clientTransaction,
			Response busyHere) {
		Dialog dialog = clientTransaction.getDialog();
		// find the call
		Call call = activeCallsRepository.findCall(dialog);

		if (call == null) {
			logger.debug("Received a stray busyHere response.");
			return;
		}

		// change status
		call.setCallState(CallState.BUSY);
	}

	/**
	 * Attempts to re-generate the corresponding request with the proper
	 * credentials and terminates the call if it fails.
	 * 
	 * @param clientTransaction
	 *            the corresponding transaction
	 * @param response
	 *            the challenge
	 * @param jainSipProvider
	 *            the provider that received the challenge
	 */
	private void processAuthenticationChallenge(
			ClientTransaction clientTransaction, Response response,
			SipProvider jainSipProvider) {
		// First find the call and the call peer that this authentication
		// request concerns.
		Call call = activeCallsRepository.findCall(clientTransaction
				.getDialog());

		try {
			logger.debug("Authenticating an INVITE request.");

			ClientTransaction retryTran = sipMgrCallback
					.getSipSecurityManager().handleChallenge(response,
							clientTransaction);

			if (retryTran == null) {
				logger.trace("No password supplied or error occured!");
				return;
			}

			if (call != null) {
				call.setDialog(retryTran.getDialog());
			}

			retryTran.sendRequest();
		} catch (Exception exc) {
			// make sure that we fail the peer in case authentication doesn't
			// go well.
			if (call != null) {
				logger.error("Failed to authenticate.", exc);
				call.setCallState(CallState.FAILED);
			}
		}
	}

	// =============================Request====================================
	/**
	 * Creates a new call and sends a RINGING response.
	 * 
	 * @param sourceProvider
	 *            the provider containing <tt>sourceTransaction</tt>.
	 * @param serverTransaction
	 *            the transaction containing the received request.
	 */
	private void processInvite(SipProvider sourceProvider,
			ServerTransaction serverTransaction) {
		// first check whether this is a reINVITE or a brand new one.
		Request invite = serverTransaction.getRequest();
		Dialog dialog = serverTransaction.getDialog();

		Call call = activeCallsRepository.createCall(dialog, invite);
		setLatestInviteTransaction(serverTransaction);
		sipMgrCallback.fireCallEvent(CallEvent.CALL_RECEIVED, call);
		// change status
		call.setCallState(CallState.ALERTING);

		ContentLengthHeader cl = invite.getContentLength();
		if (cl != null && cl.getContentLength() > 0) {
			call.setRemoteSdpDescription(new String(invite.getRawContent()));
		}

		// Are we the one they are looking for?
		URI calleeURI = ((ToHeader) invite.getHeader(ToHeader.NAME))
				.getAddress().getURI();
		if (calleeURI.isSipURI()) {
			String calleeUser = ((SipURI) calleeURI).getUser();
			String localUser = sipMgrCallback.getLocalUser();
			// user info is case sensitive according to rfc3261
			if (!calleeUser.equals(localUser)) {
				// sipMgrCallback.fireCallRejectedLocally(
				// "The user specified by the caller did not match the local user!",
				// invite
				// );
				call.setCallState(CallState.DISCONNECTED);
				Response ringing = null;
				try {
					ringing = sipMgrCallback.messageFactory.createResponse(
							Response.NOT_FOUND, invite);
					sipMgrCallback.attachToTag(ringing, dialog);
				} catch (ParseException ex) {
					call.setCallState(CallState.DISCONNECTED);
					logger.error("Failed to create a NOT_FOUND response to an INVITE request!",
									ex);
					return;
				}
				try {
					serverTransaction.sendResponse(ringing);
				} catch (SipException ex) {
					call.setCallState(CallState.DISCONNECTED);
					logger.error("Failed to send a NOT_FOUND response to an INVITE request!",
									ex);
					return;
				} catch (InvalidArgumentException ex) {
					call.setCallState(CallState.DISCONNECTED);
					logger.error("Failed to send a NOT_FOUND response to an INVITE request!",
									ex);
					return;
				}
				return;
			}
		}

		// Send RINGING
		Response ringing = null;
		try {
			ringing = sipMgrCallback.messageFactory.createResponse(
					Response.RINGING, invite);
			sipMgrCallback.attachToTag(ringing, dialog);
		} catch (ParseException ex) {
			call.setCallState(CallState.DISCONNECTED);
			logger.error("Failed to create a RINGING response to an INVITE request!",
							ex);
			return;
		}
		try {
			serverTransaction.sendResponse(ringing);
		} catch (SipException ex) {
			call.setCallState(CallState.DISCONNECTED);
			logger.error(
					"Failed to send a RINGING response to an INVITE request!",
					ex);
			return;
		} catch (InvalidArgumentException ex) {
			call.setCallState(CallState.DISCONNECTED);
			logger.error(
					"Failed to send a RINGING response to an INVITE request!",
					ex);
			return;
		}
	}

	/**
	 * Sets the state of the corresponding call peer to DISCONNECTED and sends
	 * an OK response.
	 * 
	 * @param serverTransaction
	 *            the ServerTransaction the the BYE request arrived in.
	 * @param byeRequest
	 *            the BYE request to process
	 */
	private void processBye(ServerTransaction byeTran, Request byeRequest) {
		// find the call
		Dialog dialog = byeTran.getDialog();
		Call call = activeCallsRepository.findCall(dialog);

		if (call == null) {
			logger.debug("Received a stray bye request.");
			return;
		}

		// change status
		call.setCallState(CallState.DISCONNECTED);
		// Send OK
		Response ok = null;

		try {
			ok = sipMgrCallback.getMessageFactory().createResponse(Response.OK,
					byeRequest);
			sipMgrCallback.attachToTag(ok, call.getDialog());
		} catch (ParseException ex) {
			logger.error("Error while trying to send a response to a bye", ex);
		}

		if (ok != null) {
			try {
				byeTran.sendResponse(ok);
				logger.debug("sent response " + ok);
			} catch (Exception ex) {
				logger.error("Failed to send an OK response to BYE request,"
						+ "exception was:\n", ex);
			}
		}
	}

	/**
	 * Updates the session description and sends the state of the corresponding
	 * call peer to CONNECTED.
	 * 
	 * @param serverTransaction
	 *            the transaction that the ACK was received in.
	 * @param ackRequest
	 *            the ACK <tt>Request</tt> we need to process
	 */
	private void processAck(ServerTransaction serverTransaction,
			Request ackRequest) {
		// find the call
		Call call = activeCallsRepository.findCall(serverTransaction
				.getDialog());

		if (call == null) {
			// this is most probably the ack for a killed call - don't signal it
			logger.debug("didn't find an ack's call, returning");
			return;
		}

		ContentLengthHeader contentLength = ackRequest.getContentLength();
		if ((contentLength != null) && (contentLength.getContentLength() > 0)) {
			call.setRemoteSdpDescription(new String(ackRequest
							.getRawContent()));
		}
		// change status
		call.setCallState(CallState.CONNECTED);
	}

	/**
	 * Sets the state of the specifies call peer as DISCONNECTED.
	 * 
	 * @param serverTransaction
	 *            the transaction that the cancel was received in.
	 * @param cancelRequest
	 *            the Request that we've just received.
	 */
	private void processCancel(ServerTransaction serverTransaction,
			Request cancelRequest) {
		// find the call
		Call call = activeCallsRepository.findCall(serverTransaction
				.getDialog());

		if (call == null) {
			logger.debug("received a stray CANCEL req. ignoring");
			return;
		}

		// Cancels should be OK-ed and the initial transaction - terminated
		try {
			Response ok = sipMgrCallback.getMessageFactory().createResponse(
					Response.OK, cancelRequest);

			sipMgrCallback.attachToTag(ok, call.getDialog());

			serverTransaction.sendResponse(ok);
			logger.debug("sent an ok response to a CANCEL request:\n" + ok);
		} catch (ParseException ex) {
			logger.error("Failed to create an OK Response to a CANCEL.", ex);
			call.setCallState(CallState.FAILED);
			return;
		} catch (Exception ex) {
			logger.error("Failed to send an OK Response to a CANCEL.", ex);
			call.setCallState(CallState.FAILED);
			return;
		}

		try {
			// stop the invite transaction as well
			Transaction tran = getLatestInviteTransaction();
			// should be server transaction and misplaced cancels should be
			// filtered by the stack but it doesn't hurt checking anyway
			if (!(tran instanceof ServerTransaction)) {
				logger.error("Received a misplaced CANCEL request!");
				return;
			}

			ServerTransaction inviteTran = (ServerTransaction) tran;
			Request invite = getLatestInviteTransaction().getRequest();
			Response requestTerminated = sipMgrCallback.getMessageFactory()
					.createResponse(Response.REQUEST_TERMINATED, invite);
			sipMgrCallback.attachToTag(requestTerminated, call.getDialog());

			inviteTran.sendResponse(requestTerminated);
			if (logger.isDebugEnabled())
				logger.debug("sent request terminated response:\n"
						+ requestTerminated);
		} catch (ParseException ex) {
			logger.error("Failed to create a REQUEST_TERMINATED Response to "
					+ "an INVITE request.", ex);
		} catch (Exception ex) {
			logger.error("Failed to send an REQUEST_TERMINATED Response to "
					+ "an INVITE request.", ex);
		}
	}

	/**
	 * Processes a retransmit or expiration Timeout of an underlying
	 * {@link Transaction}handled by this SipListener. This Event notifies the
	 * application that a retransmission or transaction Timer expired in the
	 * SipProvider's transaction state machine. The TimeoutEvent encapsulates
	 * the specific timeout type and the transaction identifier either client or
	 * server upon which the timeout occurred. The type of Timeout can by
	 * determined by:
	 * <tt>timeoutType = timeoutEvent.getTimeout().getValue();</tt>
	 * 
	 * @param timeoutEvent
	 *            the timeoutEvent received indicating either the message
	 *            retransmit or transaction timed out.
	 * 
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processTimeout(TimeoutEvent timeoutEvent) {
		Transaction transaction;
		if (timeoutEvent.isServerTransaction()) {
			// don't care. or maybe a stack bug?
			return false;
		} else {
			transaction = timeoutEvent.getClientTransaction();
		}

		Call call = activeCallsRepository.findCall(transaction.getDialog());

		if (call == null) {
			logger.debug("Got a headless timeout event." + timeoutEvent);
			return false;
		}

		// change status
		call.setCallState(CallState.FAILED);
		return true;
	}

	/**
	 * Create a new call and invite the specified CallPeer to it.
	 * 
	 * @param callee
	 *            the sip address of the callee that we should invite to a new
	 *            call.
	 * @return CallPeer the CallPeer that will represented by the specified uri.
	 *         All following state change events will be delivered through that
	 *         call peer. The Call that this peer is a member of could be
	 *         retrieved from the <tt>CallPeer</tt> instance with the use of the
	 *         corresponding method.
	 * 
	 * @throws OperationFailedException
	 *             with the corresponding code if we fail to create the call.
	 * @throws ParseException
	 *             if <tt>callee</tt> is not a valid sip address <tt>String</tt>
	 *             .
	 */
	public Call createCall(String callee, String sdpContent)
			throws OperationFailedException {
		callee = callee.trim();
		if (callee.toLowerCase().indexOf("sip:") == -1 // no sip scheme
				&& callee.indexOf('@') != -1 // most probably a sip uri
		)
			callee = "sip:" + callee;
		// Request URI
		SipURI requestURI;
		try {
			requestURI = (SipURI)sipMgrCallback.getAddressFactory().createURI(callee);
			if(requestURI.getTransportParam() == null){
				requestURI.setTransportParam(sipMgrCallback.getTransport());
			}
		} catch (ParseException ex) {
			throw new OperationFailedException(callee
					+ " is not a legal SIP uri!",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		// Call ID
		CallIdHeader callIdHeader = sipMgrCallback.getJainSipProvider().getNewCallId();
		// CSeq
		CSeqHeader cSeqHeader;
		try {
			long cSeq = 1;
			cSeqHeader = sipMgrCallback.getHeaderFactory().createCSeqHeader(cSeq,
					Request.INVITE);
		} catch (ParseException ex) {
			throw new OperationFailedException(
					"An unexpected erro occurred while"
							+ "constructing the CSeqHeadder",
					OperationFailedException.GENERAL_ERROR, ex);
		} catch (InvalidArgumentException ex) {
			throw new OperationFailedException(
					"An unexpected erro occurred while"
							+ "constructing the CSeqHeadder",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		// FromHeader
		FromHeader fromHeader = sipMgrCallback.getFromHeader();
		// ToHeader
		Address toAddress = sipMgrCallback.getAddressFactory().createAddress(requestURI);
		ToHeader toHeader;
		try {
			toHeader = sipMgrCallback.getHeaderFactory().createToHeader(toAddress,
					null);
		} catch (ParseException ex) {
			// Shouldn't happen
			throw new OperationFailedException(
					"Null is not an allowed tag for the to header!",
					OperationFailedException.GENERAL_ERROR);
		}
		// ViaHeaders
		ArrayList<ViaHeader> viaHeaders = sipMgrCallback.getLocalViaHeaders(toAddress);
		// MaxForwards
		MaxForwardsHeader maxForwards = sipMgrCallback.getMaxForwardsHeader();
		// Contact
		ContactHeader contactHeader = sipMgrCallback.getContactHeader();
		Request invite = null;
		try {
			invite = sipMgrCallback.messageFactory.createRequest(requestURI,
					Request.INVITE, callIdHeader, cSeqHeader, fromHeader,
					toHeader, viaHeaders, maxForwards);
		} catch (ParseException ex) {
			throw new OperationFailedException(
					"Failed to create invite Request!",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		//
		invite.addHeader(contactHeader);
		// Content
		ContentTypeHeader contentTypeHeader = null;
		try {
			contentTypeHeader = sipMgrCallback.headerFactory
					.createContentTypeHeader("applications", "sdp");
		} catch (ParseException ex) {
			// Shouldn't happen
			throw new OperationFailedException(
					"Failed to create a content type header for the INVITE request",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		try {
			invite.setContent(sdpContent, contentTypeHeader);
		} catch (ParseException ex) {
			throw new OperationFailedException(
					"Failed to parse sdp data while creating invite request!",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		// Transaction
		ClientTransaction inviteTransaction;
		try {
			inviteTransaction = sipMgrCallback.getJainSipProvider()
					.getNewClientTransaction(invite);
			setLatestInviteTransaction(inviteTransaction);
		} catch (TransactionUnavailableException ex) {
			throw new OperationFailedException(
					"Failed to create inviteTransaction.\n"
							+ "This is most probably a network connection error.",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		try {
			inviteTransaction.sendRequest();
		} catch (SipException ex) {
			throw new OperationFailedException(
					"An error occurred while sending invite request",
					OperationFailedException.GENERAL_ERROR, ex);
		}
		return activeCallsRepository.createCall(inviteTransaction.getDialog(),
				invite);
	}

	/**
	 * Indicates a user request to answer an incoming call from the specified
	 * CallPeer.
	 * 
	 * Sends an OK response to <tt>callPeer</tt>. Make sure that the call peer
	 * contains an SDP description when you call this method.
	 * 
	 * @param peer
	 *            the call peer that we need to send the ok to.
	 * @throws OperationFailedException
	 * 
	 * @throws OperationFailedException
	 *             if we fail to create or send the response.
	 * @throws ClassCastException
	 *             if <tt>peer</tt> is not an instance of a
	 *             <tt>CallPeerSipImpl</tt>
	 */
	public void answerCallPeer(String callID, String sdpContent)
			throws OperationFailedException {
		Call call = activeCallsRepository.findCall(callID);
		if (call == null) {
			throw new OperationFailedException("Failed to find call with id="
					+ callID, OperationFailedException.GENERAL_ERROR);
		}

		Dialog dialog = call.getDialog();
		if (dialog == null) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to extract call's associated dialog! Ending Call!",
					OperationFailedException.GENERAL_ERROR);
		}
		Transaction transaction = getLatestInviteTransaction();
		if (transaction == null || !(transaction instanceof ServerTransaction)) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to extract a ServerTransaction "
							+ "from the call's associated dialog!",
					OperationFailedException.INTERNAL_ERROR);
		}

		ServerTransaction serverTransaction = (ServerTransaction) transaction;
		Request invite = serverTransaction.getRequest();
		Response ok = null;
		try {
			ok = sipMgrCallback.getMessageFactory().createResponse(Response.OK,
					invite);
			sipMgrCallback.attachToTag(ok, dialog);

		} catch (ParseException ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to construct an OK response to an INVITE request",
					OperationFailedException.INTERNAL_ERROR, ex);
		}

		// Content
		ContentTypeHeader contentTypeHeader = null;
		try {
			contentTypeHeader = sipMgrCallback.getHeaderFactory()
					.createContentTypeHeader("application", "sdp");
		} catch (ParseException ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to create a content type header for the OK response",
					OperationFailedException.INTERNAL_ERROR, ex);
		}

		try {
			ok.setContent(sdpContent, contentTypeHeader);
		} catch (ParseException ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to parse sdp data while creating invite request!",
					OperationFailedException.NETWORK_FAILURE, ex);
		}
		ContactHeader contactHeader = sipMgrCallback.getContactHeader();
		ok.addHeader(contactHeader);

		try {
			serverTransaction.sendResponse(ok);
			if (logger.isDebugEnabled())
				logger.debug("sent response\n" + ok);
		} catch (Exception ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to send an OK response to an INVITE request",
					OperationFailedException.NETWORK_FAILURE, ex);
		}
	}

	public synchronized void hangupCallPeer(String callID)
			throws OperationFailedException {
		Call call = activeCallsRepository.findCall(callID);
		if (call == null) {
			throw new OperationFailedException("Failed to find call with id="
					+ callID, OperationFailedException.GENERAL_ERROR);
		}

		if (CallState.DISCONNECTED.equals(call.getCallState())
				|| CallState.FAILED.equals(call.getCallState())) {
			logger.debug("Ignoring a request to hangup a call peer "
					+ "that is already DISCONNECTED");
			return;
		}

		CallState callState = call.getCallState();
		Dialog dialog = call.getDialog();

		if (callState.equals(CallState.CONNECTED)) {
			boolean dialogIsAlive = sayBye(dialog);
			if (!dialogIsAlive) {
				call.setCallState(CallState.DISCONNECTED);
			}
		} else if (CallState.DIALING.equals(callState)
				|| CallState.RINGING.equals(callState)) {
			if (getLatestInviteTransaction() != null) {
				// Someone knows about us. Let's be polite and say we are
				// leaving
				sayCancel(dialog);
			}
			call.setCallState(CallState.DISCONNECTED);
		} else if (callState.equals(CallState.ALERTING)) {
			call.setCallState(CallState.DISCONNECTED);
			sayBusyHere(dialog);
		}
		// For FAILED and BUSY we only need to update CALL_STATUS
		else if (callState.equals(CallState.BUSY)) {
			call.setCallState(CallState.DISCONNECTED);
		} else if (callState.equals(CallState.FAILED)) {
			call.setCallState(CallState.DISCONNECTED);
		} else {
			call.setCallState(CallState.DISCONNECTED);
			logger.error("Could not determine call peer state!");
		}
	}

	private boolean sayBye(Dialog dialog) throws OperationFailedException {
		// Request request = dialog.getFirstTransaction().getRequest();
		Request bye = null;
		try {
			bye = dialog.createRequest(Request.BYE);
		} catch (SipException ex) {
			throw new OperationFailedException("Failed to create bye request!",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
		ClientTransaction clientTransaction = null;
		try {
			clientTransaction = sipMgrCallback.getJainSipProvider()
					.getNewClientTransaction(bye);
		} catch (TransactionUnavailableException ex) {
			throw new OperationFailedException(
					"Failed to construct a client transaction from the BYE request",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
		try {
			dialog.sendRequest(clientTransaction);
		} catch (SipException ex1) {
			throw new OperationFailedException(
					"Failed to send the BYE request",
					OperationFailedException.INTERNAL_ERROR);
		}
		return false;
	} // bye

	// cancel
	private void sayCancel(Dialog dialog) throws OperationFailedException {
		if (getLatestInviteTransaction() instanceof ServerTransaction) {
			logger.error("Cannot cancel a server transaction");
			throw new OperationFailedException(
					"Cannot cancel a server transaction",
					OperationFailedException.INTERNAL_ERROR);
		}

		ClientTransaction clientTransaction = (ClientTransaction) getLatestInviteTransaction();
		try {
			Request cancel = clientTransaction.createCancel();
			ClientTransaction cancelTransaction = sipMgrCallback
					.getJainSipProvider().getNewClientTransaction(cancel);
			cancelTransaction.sendRequest();
		} catch (SipException ex) {
			throw new OperationFailedException(
					"Failed to send the CANCEL request",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
	} // cancel

	// busy here
	private void sayBusyHere(Dialog dialog) throws OperationFailedException {
		if (!(getLatestInviteTransaction() instanceof ServerTransaction)) {
			logger.error("Cannot send BUSY_HERE in a client transaction");
			throw new OperationFailedException(
					"Cannot send BUSY_HERE in a client transaction",
					OperationFailedException.INTERNAL_ERROR);
		}

		Request request = getLatestInviteTransaction().getRequest();
		Response busyHere = null;
		try {
			busyHere = sipMgrCallback.getMessageFactory().createResponse(
					Response.BUSY_HERE, request);
			sipMgrCallback.attachToTag(busyHere, dialog);
		} catch (ParseException ex) {
			throw new OperationFailedException(
					"Failed to create the BUSY_HERE response!",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
		ServerTransaction serverTransaction = (ServerTransaction) getLatestInviteTransaction();

		try {
			serverTransaction.sendResponse(busyHere);
			logger.debug("sent response:\n" + busyHere);
		} catch (Exception ex1) {
			throw new OperationFailedException(
					"Failed to send the BUSY_HERE response",
					OperationFailedException.INTERNAL_ERROR);
		}
	} // busy here

	public void sayInteranlError(String callID) throws OperationFailedException {
		Call call = activeCallsRepository.findCall(callID);
		if (call == null) {
			throw new OperationFailedException("Failed to find call with id="
					+ callID, OperationFailedException.GENERAL_ERROR);
		}
		Dialog dialog = call.getDialog();
		if (dialog == null) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to extract call's associated dialog! Ending Call!",
					OperationFailedException.INTERNAL_ERROR);
		}
		Transaction transaction = getLatestInviteTransaction();
		if (transaction == null || !(transaction instanceof ServerTransaction)) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to extract a ServerTransaction "
							+ "from the call's associated dialog!",
					OperationFailedException.INTERNAL_ERROR);
		}

		ServerTransaction serverTransaction = (ServerTransaction) transaction;
		Response internalError = null;

		try {
			internalError = sipMgrCallback.getMessageFactory().createResponse(
					Response.SERVER_INTERNAL_ERROR,
					serverTransaction.getRequest());
			sipMgrCallback.attachToTag(internalError, dialog);
		} catch (ParseException ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to construct an OK response to an INVITE request",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
		ContactHeader contactHeader = sipMgrCallback.getContactHeader();
		internalError.addHeader(contactHeader);
		try {
			serverTransaction.sendResponse(internalError);
		} catch (Exception ex) {
			call.setCallState(CallState.DISCONNECTED);
			throw new OperationFailedException(
					"Failed to send an OK response to an INVITE request",
					OperationFailedException.INTERNAL_ERROR, ex);
		}
	}

	/**
	 * Verifies that our protocol provider is properly registered and throws an
	 * <tt>OperationFailedException</tt> if that's not the case.
	 * 
	 * @throws OperationFailedException
	 *             if the protocol provider that created us is not registered.
	 */
	private void assertStarted() throws OperationFailedException {
		if (!sipMgrCallback.isStarted()) {
			throw new OperationFailedException(
					"The protocol provider should be started "
							+ "before placing an outgoing call.",
					OperationFailedException.PROVIDER_NOT_REGISTERED);
		}
	}
}
